package besta.moon.engine;

import java.util.*;
import java.io.*;
import java.nio.channels.FileChannel;
import java.sql.*;

import besta.moon.MoonConnection;
import besta.moon.command.CommandCreateDatabase;
import besta.moon.engine.index.IndexDescription;
import besta.moon.engine.index.IndexDescriptions;
import besta.moon.engine.table.Column;
import besta.moon.engine.table.Columns;
import besta.moon.engine.table.ForeignKey;
import besta.moon.engine.table.ForeignKeys;
import besta.moon.sql.datatype.Strings;
import besta.moon.sql.parser.SQLTokenizer;
import besta.moon.MoonException;
import besta.moon.MoonCode;
import besta.moon.engine.table.TableViewMap;
import besta.moon.util.MoonUtils;


public final class Database {

    private static final HashMap<String, Database> databases = new HashMap<>();

    private final TableViewMap tableViews = new TableViewMap();
    private final String name;
    private final boolean readonly;
    private final File directory;
    private final FileChannel master;
    private final WeakHashMap<MoonConnection, ?> connections = new WeakHashMap();

    public static Database getDatabase(String name, MoonConnection con, boolean create) throws SQLException {
        if (name == null) {
            return null;
        }
        if (name.startsWith("file:")) {
            name = name.substring(5);
        }
        File file;
        try {
            file = new File(name).getCanonicalFile();
        } catch (IOException th) {
            throw MoonException.createFromException(th);
        }
        String dbKey = file.getName() + ";readonly=" + con.isReadOnly();
        synchronized (databases) {
            Database db = (Database) databases.get(dbKey);
            if (db == null) {
                if (create && !file.isDirectory()) {
                    CommandCreateDatabase command = new CommandCreateDatabase(con.log, name);
                    command.execute(con, null);
                }
                db = new Database(name, file, con.isReadOnly());
                databases.put(dbKey, db);
            }
            db.connections.put(con, null);
            return db;
        }
    }

    private static Database getDatabase(MoonConnection con, String name) throws SQLException {
        return name == null ? con.getDatabase(false) : getDatabase(name, con, false);
    }

    private Database(String name, File canonicalFile, boolean readonly) throws SQLException {
        try {
            this.name = name;
            this.readonly = readonly;
            directory = canonicalFile;
            if (!directory.isDirectory()) {
                throw MoonException.create(MoonCode.DB_NONEXISTENT, name);
            }
            File file = new File(directory, MoonUtils.MASTER_FILENAME);
            if (!file.exists()) {
                throw MoonException.create(MoonCode.DB_NOT_DIRECTORY, name);
            }
            master = MoonUtils.openRaFile(file, readonly);
        } catch (Exception e) {
            throw MoonException.createFromException(e);
        }
    }

    public String getName() {
        return name;
    }

    public boolean isReadOnly() {
        return readonly;
    }


    public static final void closeConnection(MoonConnection con) throws SQLException {
        synchronized (databases) {
            Iterator<Database> iterator = databases.values().iterator();
            while (iterator.hasNext()) {
                Database database = (Database) iterator.next();
                WeakHashMap<?, ?> connections = database.connections;
                connections.remove(con);
                if (connections.size() == 0) {
                    try {
                        iterator.remove();
                        database.close();
                    } catch (Exception e) {
                        throw MoonException.createFromException(e);
                    }
                }
            }
        }
    }


    private final void close() throws Exception {
        synchronized (tableViews) {
            Iterator iterator = tableViews.values().iterator();
            while (iterator.hasNext()) {
                View tableView = (View) iterator.next();
                tableView.close();
                iterator.remove();
            }
        }
        master.close();
    }

    public static View getTableView(MoonConnection con, String catalog, String tableName) throws SQLException {
        return getDatabase(con, catalog).getTableView(con, tableName);
    }

    public View getTableView(MoonConnection con, String tableName) throws SQLException {
        synchronized (tableViews) {
            View tableView = tableViews.get(tableName);
            if (tableView == null) {
                tableView = View.load(con, this, tableName);
                tableViews.put(tableName, tableView);
            }
            return tableView;
        }
    }

    public static void dropTable(MoonConnection con, String catalog, String tableName) throws Exception {
        getDatabase(con, catalog).dropTable(con, tableName);
    }

    public void dropTable(MoonConnection con, String tableName) throws Exception {
        synchronized (tableViews) {
            Table table = (Table) tableViews.get(tableName);
            if (table != null) {
                tableViews.remove(tableName);
                table.drop(con);
            } else {
                Table.drop(this, tableName);
            }
        }
    }

    public void removeTableView(String tableViewName) {
        synchronized (tableViews) {
            tableViews.remove(tableViewName);
        }
    }

    public void replaceTable(Table oldTable, Table newTable) throws Exception {
        synchronized (tableViews) {
            tableViews.remove(oldTable.name);
            tableViews.remove(newTable.name);
            oldTable.close();
            newTable.close();
            File oldFile = oldTable.getFile(this);
            File newFile = newTable.getFile(this);
            File tmpFile = new File(MoonUtils.createTableViewFileName(this,
                    "#" + System.currentTimeMillis() + this.hashCode()));
            if (!oldFile.renameTo(tmpFile)) {
                throw MoonException.create(MoonCode.TABLE_CANT_RENAME, oldTable.name);
            }
            if (!newFile.renameTo(oldFile)) {
                tmpFile.renameTo(oldFile);
                throw MoonException.create(MoonCode.TABLE_CANT_RENAME, oldTable.name);
            }
            tmpFile.delete();
        }
    }

    public static void dropView(MoonConnection con, String catalog, String tableName) throws Exception {
        getDatabase(con, catalog).dropView(tableName);
    }

    void dropView(String viewName) throws Exception {
        synchronized (tableViews) {
            Object view = tableViews.remove(viewName);
            if (view != null && !(view instanceof ViewTable)) {
                throw MoonException.create(MoonCode.VIEWDROP_NOT_VIEW, viewName);
            }

            ViewTable.drop(this, viewName);
        }
    }

    private void checkForeignKeys(MoonConnection con, ForeignKeys foreignKeys) throws SQLException {
        for (int i = 0; i < foreignKeys.size(); i++) {
            ForeignKey foreignKey = foreignKeys.get(i);
            View pkTable = getTableView(con, foreignKey.pkTable);
            if (!(pkTable instanceof Table)) {
                throw MoonException.create(MoonCode.FK_NOT_TABLE, foreignKey.pkTable);
            }
        }
    }

    public void createTable(MoonConnection con, String name, Columns columns, IndexDescriptions indexes,
            ForeignKeys foreignKeys) throws Exception {
        checkForeignKeys(con, foreignKeys);
        Table table = new Table(this, con, name, columns, indexes, foreignKeys);
        synchronized (tableViews) {
            tableViews.put(name, table);
        }
    }

    public Table createTable(MoonConnection con, String tableName, Columns columns, IndexDescriptions oldIndexes,
            IndexDescriptions newIndexes, ForeignKeys foreignKeys) throws Exception {
        checkForeignKeys(con, foreignKeys);
        Table table = new Table(this, con, tableName, columns, oldIndexes, newIndexes, foreignKeys);
        synchronized (tableViews) {
            tableViews.put(tableName, table);
        }
        return table;
    }

    public void createView(MoonConnection con, String viewName, String sql) throws Exception {
        new ViewTable(this, con, viewName, sql);
    }

    public static Object[][] getCatalogs(Database database) {
        List<Object[]> catalogs = new ArrayList<Object[]>();
        File baseDir = (database != null) ? database.directory.getParentFile() : new File(".");
        File dirs[] = baseDir.listFiles();
        if (dirs != null) {
            for (int i = 0; i < dirs.length; i++) {
                if (dirs[i].isDirectory()) {
                    if (new File(dirs[i], MoonUtils.MASTER_FILENAME).exists()) {
                        Object[] catalog = new Object[1];
                        catalog[0] = dirs[i].getPath();
                        catalogs.add(catalog);
                    }
                }
            }
        }
        Object[][] result = new Object[catalogs.size()][];
        catalogs.toArray(result);
        return result;
    }

    public Strings getTables(String tablePattern) {
        Strings list = new Strings();
        File dirs[] = directory.listFiles();
        if (dirs != null) {
            if (tablePattern == null) {
                tablePattern = "%";
            }
        }
        tablePattern += MoonUtils.TABLE_VIEW_EXTENTION;
        for (int i = 0; i < dirs.length; i++) {
            String name = dirs[i].getName();
            if (MoonUtils.like(name, tablePattern)) {
                list.add(name.substring(0, name.length() - MoonUtils.TABLE_VIEW_EXTENTION.length()));
            }
        }
        return list;
    }

    public Object[][] getColumns(MoonConnection con, String tablePattern, String colPattern) throws Exception {
        List<Object[]> rows = new ArrayList<Object[]>();
        Strings tables = getTables(tablePattern);
        for (int i = 0; i < tables.size(); i++) {
            String tableName = tables.get(i);
            try {
                View tab = getTableView(con, tableName);
                Columns cols = tab.columns;
                for (int c = 0; c < cols.size(); c++) {
                    Column col = cols.get(c);
                    Object[] row = new Object[18];
                    row[0] = getName(); 
                    row[2] = tableName; 
                    row[3] = col.getName(); 
                    row[4] = MoonUtils.getShort(SQLTokenizer.getSQLDataType(col.getDataType())); 
                    row[5] = SQLTokenizer.getKeyWord(col.getDataType()); 
                    row[6] = MoonUtils.getInteger(col.getColumnSize());
                    row[8] = MoonUtils.getInteger(col.getScale());
                    row[9] = MoonUtils.getInteger(10); 
                    row[10] = MoonUtils.getInteger(col.isNullable() ? DatabaseMetaData.columnNullable
                            : DatabaseMetaData.columnNoNulls); 
                    row[12] = col.getDefaultDefinition(); 
                          row[15] = row[6]; 
                    row[16] = MoonUtils.getInteger(i); 
                    row[17] = col.isNullable() ? "YES" : "NO"; 
                    rows.add(row);
                }
            } catch (Exception e) {
       
            }
        }
        Object[][] result = new Object[rows.size()][];
        rows.toArray(result);
        return result;
    }

    public Object[][] getReferenceKeys(MoonConnection con, String pkTable, String fkTable) throws SQLException {
        List rows = new ArrayList();
        Strings tables = (pkTable != null) ? getTables(pkTable) : getTables(fkTable);
        for (int t = 0; t < tables.size(); t++) {
            String tableName = tables.get(t);
            View tab = getTableView(con, tableName);
            if (!(tab instanceof Table)) {
                continue;
            }
            ForeignKeys references = ((Table) tab).references;
            for (int i = 0; i < references.size(); i++) {
                ForeignKey foreignKey = references.get(i);
                IndexDescription pk = foreignKey.pk;
                IndexDescription fk = foreignKey.fk;
                if ((pkTable == null || pkTable.equals(foreignKey.pkTable))
                        && (fkTable == null || fkTable.equals(foreignKey.fkTable))) {
                    Strings columnsPk = pk.getColumns();
                    Strings columnsFk = fk.getColumns();
                    for (int c = 0; c < columnsPk.size(); c++) {
                        Object[] row = new Object[14];
                        row[0] = getName(); 
                        row[2] = foreignKey.pkTable; 
                        row[3] = columnsPk.get(c); 
                        row[4] = getName(); 
                        row[6] = foreignKey.fkTable; 
                        row[7] = columnsFk.get(c); 
                        row[8] = MoonUtils.getShort(c + 1); 
                        row[9] = MoonUtils.getShort(foreignKey.updateRule);
                        row[10] = MoonUtils.getShort(foreignKey.deleteRule); 
                        row[11] = fk.getName(); 
                        row[12] = pk.getName(); 
                        row[13] = MoonUtils.getShort(DatabaseMetaData.importedKeyNotDeferrable); 
                        rows.add(row);
                    }
                }
            }
        }
        Object[][] result = new Object[rows.size()][];
        rows.toArray(result);
        return result;
    }

    public Object[][] getBestRowIdentifier(MoonConnection con, String table) throws SQLException {
        List rows = new ArrayList();
        Strings tables = getTables(table);
        for (int t = 0; t < tables.size(); t++) {
            String tableName = tables.get(t);
            View tab = getTableView(con, tableName);
            if (!(tab instanceof Table)) {
                continue;
            }
            IndexDescriptions indexes = ((Table) tab).indexes;
            for (int i = 0; i < indexes.size(); i++) {
                IndexDescription index = indexes.get(i);
                if (index.isUnique()) {
                    Strings columns = index.getColumns();
                    for (int c = 0; c < columns.size(); c++) {
                        String columnName = columns.get(c);
                        Column column = tab.findColumn(columnName);
                        Object[] row = new Object[8];
                        row[0] = MoonUtils.getShort(DatabaseMetaData.bestRowSession);
                        row[1] = columnName; 
                        final int dataType = column.getDataType();
                        row[2] = MoonUtils.getInteger(dataType);
                        row[3] = SQLTokenizer.getKeyWord(dataType);
                        row[4] = MoonUtils.getInteger(column.getPrecision()); 
                        row[6] = MoonUtils.getShort(column.getScale());
                        row[7] = MoonUtils.getShort(DatabaseMetaData.bestRowNotPseudo);
                        rows.add(row);
                    }
                }
            }
        }
        Object[][] result = new Object[rows.size()][];
        rows.toArray(result);
        return result;
    }

    public Object[][] getPrimaryKeys(MoonConnection con, String table) throws SQLException {
        List rows = new ArrayList();
        Strings tables = getTables(table);
        for (int t = 0; t < tables.size(); t++) {
            String tableName = tables.get(t);
            View tab = getTableView(con, tableName);
            if (!(tab instanceof Table)) {
                continue;
            }
            IndexDescriptions indexes = ((Table) tab).indexes;
            for (int i = 0; i < indexes.size(); i++) {
                IndexDescription index = indexes.get(i);
                if (index.isPrimary()) {
                    Strings columns = index.getColumns();
                    for (int c = 0; c < columns.size(); c++) {
                        Object[] row = new Object[6];
                        row[0] = getName();
                        row[2] = tableName; 
                        row[3] = columns.get(c); 
                        row[4] = MoonUtils.getShort(c + 1);
                        row[5] = index.getName();
                        rows.add(row);
                    }
                }
            }
        }
        Object[][] result = new Object[rows.size()][];
        rows.toArray(result);
        return result;
    }

    public Object[][] getIndexInfo(MoonConnection con, String table, boolean unique) throws SQLException {
        List rows = new ArrayList();
        Strings tables = getTables(table);
        Short type = MoonUtils.getShort(DatabaseMetaData.tableIndexOther);
        for (int t = 0; t < tables.size(); t++) {
            String tableName = tables.get(t);
            View tab = getTableView(con, tableName);
            if (!(tab instanceof Table)) {
                continue;
            }
            IndexDescriptions indexes = ((Table) tab).indexes;
            for (int i = 0; i < indexes.size(); i++) {
                IndexDescription index = indexes.get(i);
                Strings columns = index.getColumns();
                for (int c = 0; c < columns.size(); c++) {
                    Object[] row = new Object[13];
                    row[0] = getName(); 
                    row[2] = tableName; 
                    row[3] = !index.isUnique();
                    row[5] = index.getName();
                    row[6] = type; 
                    row[7] = MoonUtils.getShort(c + 1); 
                    row[8] = columns.get(c); 
                    rows.add(row);
                }
            }
        }
        Object[][] result = new Object[rows.size()][];
        rows.toArray(result);
        return result;
    }
}
